fix(eval): close raw tunnels at the layer that owns the sockets - #3017
Conversation
liugddx
left a comment
There was a problem hiding this comment.
The follow-up is aligned with the parent PR review: removing the unused tcp_start/tcp_message flow.kill hooks, keeping a single pre-relay next_layer path, using isinstance, and asserting both CloseConnection commands are all good changes.
One issue remains before merge: constructing CloseRawLayer through Layer.init appends it to context.layers without removing the TCPLayer being replaced, leaving a stale protocol layer in the stack. The test uses a fake that does not register itself in context.layers, so it misses this behavior.
Please preserve the layer-stack invariant and extend the test accordingly. After that correction, this is ready to merge.
中文评审结论
这个 follow-up 与父 PR 的评审结论一致:删除未使用的 tcp_start/tcp_message flow.kill hooks、只保留 relay 前的 next_layer 阻断路径、使用 isinstance,以及断言 client/server 两个 CloseConnection 命令,都是正确的改动。
合并前还剩一个问题:CloseRawLayer 通过 Layer.init 构造时会被追加到 context.layers,但被替换的 TCPLayer 没有移除,导致协议层栈残留旧层。当前测试使用的 fake 不会把自己注册到 context.layers,因此没有发现这一行为。
请保持层栈不变量并补充对应测试。修正后即可合并。
Astro-Han
left a comment
There was a problem hiding this comment.
A fresh live probe changes the conclusion from the earlier review: removing tcp_start and tcp_message is not safe with the current replacement.
In mitmproxy 12.2.3, script addons receive next_layer before the built-in classifier has populated nextlayer.layer. A live probe showed nextlayer.layer was None at both script hook calls, so the isinstance(current, TCPLayer) branch never ran. After this PR removed the TCP hooks, a CONNECT raw tunnel relayed bytes in both directions and produced no raw_tunnel audit record.
HTTP 101 upgrades are a separate path. Mitmproxy constructs their TCPLayer inside the HTTP layer without another next_layer hook. The same probe confirmed bidirectional raw payload and no audit record for that path as well.
This also changes the earlier layer-stack finding. The stale TCPLayer is real, but it is secondary: the closer is not reached on either production path. Fixing the stack alone would not make this safe.
The current test hides the problem by constructing a fake with .layer already set. In production it is still None when the script hook runs. CI also runs this test without mitmproxy installed, so the fallback imports and fake types do not verify the real addon order or connection behavior.
I suggest keeping the existing TCP hooks until an equivalent replacement has been exercised through a real proxy. A complete replacement needs to cover both raw-protocol selection and HTTP 101 upgrades. One clean option is to let mitmproxy's native rawtcp=false setting own the fail-closed guarantee, then keep narrow hooks for audit. If the custom closer remains, it should run only after the built-in classifier has selected TCPLayer, remove that layer from context.layers, and handle the 101 path separately.
The verification should use mitmproxy 12.2.3 and assert zero relayed bytes, the expected audit record, connection closure, normal HTTPS behavior, and WebSocket compatibility.
I used Claude for the live mitmproxy probe and Codex reviewers for independent source and lifecycle checks, then verified the current head and CI state.
中文审查结论
新的真实代理探针改变了此前 review 的结论:在当前替代方案下,删除 tcp_start 和 tcp_message 并不安全。
在 mitmproxy 12.2.3 中,script addon 收到 next_layer hook 时,内置 classifier 还没有为 nextlayer.layer 赋值。真实代理探针显示两次 script hook 中的 nextlayer.layer 都是 None,因此 isinstance(current, TCPLayer) 分支从未执行。这个 PR 删除 TCP hooks 后,CONNECT raw tunnel 可以双向传输字节,也没有产生 raw_tunnel 审计记录。
HTTP 101 upgrade 是另一条独立路径。Mitmproxy 会直接在 HTTP layer 内构造 TCPLayer,不会再次触发当前脚本依赖的 next_layer hook。真实探针同样确认这条路径可以双向传输 raw payload,且没有审计记录。
这也改变了之前 layer stack finding 的优先级。旧 TCPLayer 残留确实存在,但它只是次要问题:当前 closer 在两条生产路径上都没有执行。只修复层栈不能解决实际问题。
当前测试通过预先设置 fake 的 .layer 避开了真实生命周期。生产中脚本执行 hook 时该字段仍然是 None。CI 又是在没有安装 mitmproxy 的环境中运行测试,因此 fallback import 和 fake type 无法验证真实 addon 顺序或连接行为。
建议在等价替代方案经过真实代理验证之前,先保留现有 TCP hooks。完整替代方案必须同时覆盖 raw protocol selection 和 HTTP 101 upgrade。一种更干净的选择是让 mitmproxy 原生的 rawtcp=false 负责 fail-closed,再用较窄的 hooks 负责审计。如果继续保留自定义 closer,它只能在内置 classifier 已经选择 TCPLayer 后运行,需要从 context.layers 移除旧层,并单独覆盖 HTTP 101 路径。
验证应使用真实 mitmproxy 12.2.3,并断言零 payload、正确的审计记录、连接关闭、正常 HTTPS 不受影响,以及 WebSocket 兼容。
本次使用 Claude 完成真实 mitmproxy 探针,并使用 Codex reviewers 独立核对源码和生命周期;随后核对了当前 head 与 CI 状态。
|
Addressed in 06cf801. Astro-Han's live probe is right: in mitmproxy 12.2.3 the script
|
|
Live verification is now in-repo as
That is why this PR keeps CI still skips this suite unless the env is set, same as the namespace test — GitHub runners do not have the pinned image. |
f5e42b3 to
d5d2a9b
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for following up with real mitmproxy coverage and for restoring the TCP hooks after the production-order evidence. The current head does prevent payload relay for raw CONNECT and HTTP 101 paths without breaking ordinary HTTP(S) or WebSocket traffic.
I found two remaining P2 issues before approval; the inline comments contain the reproductions and requested verification.
From first principles, the remaining production problem is ownership of connection closure. The current next_layer -> CloseRawLayer branch is not reached in the observed addon order because the script hook sees layer=None, while the reachable tcp_start / tcp_message path uses TCPFlow.kill(), which marks the flow but does not close either transport. A long-lived peer can therefore retain both sockets even though payload is suppressed. Please make one production-reachable layer/connection seam the sole closing authority, then remove the superseded parallel path and its fake-only tests.
The live harness belongs in this PR because unit fakes cannot prove the mitmproxy lifecycle, but it should assert proxy-initiated EOF/reset against a long-lived origin and isolate each run's Docker resources. Once the final path is settled, please also update the PR title and body: they still describe the earlier next_layer-only design and removal of the TCP hooks.
I verified 14/14 focused Python unit tests and 5/5 live tests on the pinned image, then reproduced the open-socket behavior with long-lived upstreams. CI is green, although the opt-in live suite is skipped by default. I found no P0 or P1 issue.
中文对照
感谢补充真实 mitmproxy 验证,也感谢根据生产 addon 顺序的证据恢复 TCP hooks。当前 head 已经能阻止 raw CONNECT 和 HTTP 101 的 payload 转发,同时不影响普通 HTTP(S) 与 WebSocket。
合并前仍有两项 P2,具体复现和验证要求见行内评论。
从第一性原理看,剩余的生产问题是连接关闭 authority。当前 next_layer -> CloseRawLayer 分支在真实 addon 顺序中不可达,因为 script hook 看到的是 layer=None;实际可达的 tcp_start / tcp_message 路径调用 TCPFlow.kill(),它只标记 flow,不会关闭两端 transport。因此即使 payload 被清空,长驻 peer 仍能持续占用两端 socket。请让一个生产可达的 layer/connection seam 成为唯一关闭 authority,并删除被替代的并行路径和 fake-only tests。
真实代理 harness 应与本 PR 一起交付,因为 unit fake 无法证明 mitmproxy 生命周期;但它需要针对长驻 origin 明确断言 proxy 主动产生 EOF/reset,并隔离每次运行的 Docker 资源。最终路径确定后,也请更新 PR 标题和正文;当前文字仍描述已被推翻的 next_layer-only、删除 TCP hooks 的方案。
我验证了 14/14 Python unit tests 和 pinned image 上的 5/5 live tests,并使用长驻 upstream 复现了 socket 保持打开的行为。CI 当前全绿,但 opt-in live suite 默认会跳过。没有发现 P0 或 P1。
Disclosure: I used three Codex reviewers and Claude for independent lifecycle, test-authenticity, and simplification checks. I reproduced the surviving findings and own this review; AI assistance does not replace the repository's required independent human review.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review. 📝 WalkthroughSummaryThis PR fixes raw tunnel handling in the evaluation egress proxy. Raw CONNECT traffic and non-WebSocket HTTP 101 upgrades now close without relaying bytes. Normal HTTP, HTTPS, and WebSocket traffic remains supported. The proxy records blocked upgrade cases as The change extends the existing egress-filter path. It does not create a parallel production proxy path. The live Docker suite adds a separate validation path for integration coverage. The solution is the smallest coherent design identified for mitmproxy’s event order. No production code can be removed without weakening raw-traffic handling or regression coverage. The focused tests cover classification, passthrough, closure, auditing, layer replacement, and layer cleanup. The live suite covers HTTP, HTTPS, blocked CONNECT, raw CONNECT, HTTP 101, and WebSocket behavior. Validation
Review-relevant risks
The person performing the merge reviews the final diff. A maintainer makes the final determination. WalkthroughThe egress proxy disables raw TCP relay, classifies raw traffic, closes matching tunnels, and audits non-WebSocket HTTP 101 upgrades. Unit tests and an opt-in Docker test validate blocked and preserved protocols. ChangesEgress proxy filtering
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🟡 Moderate · up to The proxy can still classify a fragmented plaintext HTTP request as raw TCP and close it before the request is complete, potentially rejecting valid traffic; the live audit check can also pass only because an earlier test created the audit file. These bounded issues should be fixed or explicitly accepted before merge. Sequence Diagram(s)sequenceDiagram
participant LiveEgressFilterTest
participant mitmdump
participant egress_filter
participant OriginServer
LiveEgressFilterTest->>mitmdump: Send proxy request
mitmdump->>egress_filter: Classify layer or response
egress_filter->>OriginServer: Forward admitted traffic
egress_filter->>mitmdump: Close raw traffic and write raw_tunnel audit
LiveEgressFilterTest->>OriginServer: Check received byte counts
LiveEgressFilterTest->>mitmdump: Check audit JSONL records
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Addressed the remaining review items in 6605e24. Closing authority
Live harness
PR title/body updated to match this design. Verified: unit 17/17; live 5/5 on |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/eval/harbor/egress_filter.py (1)
307-316: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe constructor carries test-only fallbacks.
Both guards exist for unit fixtures, not for production. A real mitmproxy
Contextalways haslayersandoptions. The syntheticOptionsclass also hides a genuine misconfiguration behindproxy_debug = False.Consider moving the shaping into the tests: build the fake context with
layers=[]and an options stub. ThenCloseRawLayer.__init__reduces to theLayer is objectfallback plussuper().__init__(context).As per path instructions: "Flag concrete cases where code can be deleted or simplified."
Source: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f16ed91a-283f-4be2-a6ab-f32c9c364128
📒 Files selected for processing (6)
packages/eval/README.mdpackages/eval/harbor/egress-proxy/entrypoint.shpackages/eval/harbor/egress_filter.pypackages/eval/harbor/test_egress_filter.pypackages/eval/harbor/test_egress_filter_live.pypackages/eval/package.json
Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.
6605e24 to
71004e1
Compare
There was a problem hiding this comment.
Pull request overview
This PR adjusts the eval egress proxy’s handling of “raw tunnel” situations so that raw CONNECT and non-WebSocket HTTP 101 upgrades are closed via CloseConnection at the layer that owns the sockets, rather than relying on TCPFlow.kill() after bytes may already have been relayed.
Changes:
- Add a
CloseRawLayerandnext_layerpre-classification logic to close raw TCP early and ensure both client/server transports are closed. - Force
rawtcp=false(entrypoint + addonconfigure) and audit non-WebSocket HTTP 101 upgrades asraw_tunnel. - Add unit tests for the new next-layer behavior and an opt-in live Docker harness to validate behavior against the pinned mitmproxy image.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/eval/README.md | Document the new opt-in live egress proxy test and what it validates. |
| packages/eval/package.json | Add the live egress proxy test script to test:dist (test is skip-gated by env var). |
| packages/eval/harbor/test_egress_filter.py | Add unit coverage for next_layer raw detection, TCPLayer replacement, and 101 upgrade auditing. |
| packages/eval/harbor/test_egress_filter_live.py | New opt-in integration test that runs the pinned proxy image and asserts raw tunnels relay zero bytes while HTTPS/WebSocket still work. |
| packages/eval/harbor/egress-proxy/entrypoint.sh | Set rawtcp=false when starting mitmdump. |
| packages/eval/harbor/egress_filter.py | Implement configure, response auditing for 101 upgrades, early raw-TCP detection, and CloseRawLayer socket closure. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/eval/README.md:137
- The README lists prerequisites for the opt-in live egress test but omits the
curldependency used for the HTTPS forwarding assertion, which can lead to confusing failures when opting in.
`MAKA_EVAL_EGRESS_PROXY_TEST=1 python3 harbor/test_egress_filter_live.py` starts the
pinned mitmproxy image and asserts raw CONNECT and HTTP 101 upgrades relay no
bytes, write `raw_tunnel`, and leave HTTPS and WebSocket working; it needs a Docker
daemon, the pinned image, `python:3.12-slim`, and outbound network, and skips otherwise. This URL policy is a blocklist for known
packages/eval/harbor/test_egress_filter_live.py:359
test_https_and_plain_http_still_forwardshells out tocurl, but the live suite doesn't check thatcurlis installed. If someone opts in withMAKA_EVAL_EGRESS_PROXY_TEST=1on a minimal host, this will fail withFileNotFoundErrorinstead of a clear skip message.
curl = subprocess.run(
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/eval/harbor/egress_filter.py (1)
319-327: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the context back-filling into the test doubles.
Lines 323-326 exist to make
SimpleNamespace(..., layers=[], options=None)doubles work, because mitmproxy'sLayer.__init__readscontext.options.proxy_debug. In production, a mitmproxyContextalways carrieslayersandoptions. The fabricatedOptionsstub therefore only hides a broken context: any later option read on the stub raisesAttributeErrorat an unrelated point instead of failing at construction.Let the unit tests pass a minimal options double and keep the constructor to the mitmproxy contract.
♻️ Proposed simplification
class CloseRawLayer(Layer): def __init__(self, context: object) -> None: if Layer is object: self.context = context return - if getattr(context, "layers", None) is None: - context.layers = [] - if getattr(context, "options", None) is None: - context.options = type("Options", (), {"proxy_debug": False})() super().__init__(context)Then, in
packages/eval/harbor/test_egress_filter.py, build contexts withoptions=SimpleNamespace(proxy_debug=False)andlayers=[]instead ofoptions=None.As per path instructions: "Determine whether it is the smallest coherent solution at the existing source of truth. Flag concrete cases where code can be deleted or simplified."
Source: Path instructions
packages/eval/harbor/test_egress_filter_live.py (1)
356-380: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRecord the outbound internet dependency, or serve TLS from the origin.
This test reaches
https://example.com/through the proxy. The rest of the suite runs entirely inside the run-scoped Docker network. On an isolated or egress-filtered host,curlreturns a non-200 code and the test fails for a reason unrelated to raw tunnel handling. The module docstring lists only Docker and the two images as prerequisites.Pick one: add the network requirement to the docstring, or terminate TLS on the origin container and point
curlat it so the whole suite stays local.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ccf62aa8-1427-4bf7-8128-68b657206e1f
📒 Files selected for processing (6)
packages/eval/README.mdpackages/eval/harbor/egress-proxy/entrypoint.shpackages/eval/harbor/egress_filter.pypackages/eval/harbor/test_egress_filter.pypackages/eval/harbor/test_egress_filter_live.pypackages/eval/package.json
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/eval/package.json
- packages/eval/README.md
- packages/eval/harbor/egress-proxy/entrypoint.sh
- packages/eval/harbor/test_egress_filter.py
Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/eval/harbor/test_egress_filter_live.py:163
- The live test shell-outs to
curlfor the HTTPS probe, but the suite never checks thatcurlis installed. WhenMAKA_EVAL_EGRESS_PROXY_TEST=1is set on a host without curl, this will raise FileNotFoundError instead of cleanly skipping with an actionable message.
if not docker_image_present(ORIGIN_IMAGE):
raise unittest.SkipTest(f"{ORIGIN_IMAGE} is not present")
packages/eval/README.md:138
- The new live-test description runs directly into the broader policy explanation ("skips otherwise. This URL policy…") in the same paragraph, which makes it read like a requirement/behavior of the live test instead of the egress policy overall. Splitting this into a new paragraph improves clarity.
`MAKA_EVAL_EGRESS_PROXY_TEST=1 python3 harbor/test_egress_filter_live.py` starts the
pinned mitmproxy image and asserts raw CONNECT and HTTP 101 upgrades relay no
bytes, write `raw_tunnel`, and leave HTTPS and WebSocket working; it needs a Docker
daemon, the pinned image, `python:3.12-slim`, and outbound network, and skips otherwise. This URL policy is a blocklist for known
benchmark and public-solution contamination surfaces, not a complete defense against a deliberately
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for moving raw-tunnel rejection to the layer that actually owns the sockets. I reviewed this head with two independent @reviewer passes plus a read-only ollama-cloud/deepseek-v4-flash:high pass, and checked the disputed path against mitmproxy 12.2.3's classifier/event ordering.
The problem is correctly defined and CloseRawLayer is the right authority. The remaining correctness gap is that the pre-classification decision is binary even when the protocol prefix is still incomplete: a one- or two-byte TLS ClientHello can be irreversibly classified as raw before the third byte arrives. Current CI is green, but the existing tests only cover a complete TLS prefix and cannot exercise this TCP fragmentation.
The smallest first-principles fix is a tri-state classifier at this seam: confirmed HTTP/TLS, confirmed raw, or undecided. Keep buffering while a prefix can still become a TLS record or HTTP request, and install CloseRawLayer only for confirmed raw traffic. Once that is the sole production authority, remove the unreachable post-classifier replacement branch and its fake-only test.
No local repository test suite was run by Codex; one reviewer performed focused source-level verification against the pinned mitmproxy implementation. Codex coordinated the passes and performed the final adjudication; external-model output was treated as unverified until checked against the code.
中文摘要
感谢把 raw tunnel 拒绝移动到真正拥有 socket 的 layer。问题定义正确,CloseRawLayer 也是正确权威。
当前仍有一个正确性缺口:协议前缀尚不完整时,分类器已经做了不可逆的二元决策。TLS ClientHello 首次只到达 1–2 字节时,会在第三个字节到达前被当成 raw 并关闭。当前 CI 虽然全绿,但测试只覆盖完整 TLS 前缀,无法覆盖 TCP fragmentation。
最小方案是在同一 seam 使用三态分类:确认 HTTP/TLS、确认 raw、未决。只要前缀仍可能成为 TLS record 或 HTTP request 就继续等待,只有确认 raw 才安装 CloseRawLayer。随后删除不可达的 post-classifier replacement 分支和 fake-only 测试,保持单一权威。
Codex 未运行本地仓库测试套件;其中一位 reviewer 针对锁定的 mitmproxy 实现做了聚焦源码验证。Codex 完成最终判断,外部模型输出在核对代码前均视为未验证输入。
|
/agentic_review |
Code Review by Qodo
1. Fragmented TLS gets closed
|
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks @1625567290 — reviewed at exact head 83a4283b24470c214367fa956b4dd4c74ec8ef36. One P2 and one P3; no P0/P1.
The ownership change itself reads correctly: assigning CloseRawLayer while the layer is still None puts the close on the path the production hook actually takes, and the resulting CloseConnection for both client and server is handled by mitmproxy's command loop rather than by killing the flow and leaving the socket open. Double-close is safe on that side — once a connection's transport has been popped, the command is a no-op, and close_connection cancels an already-cancelled handler idempotently. So the peer-disconnected-first and both-paths-close-at-once cases do not leak or throw.
The unit tests are good where they apply: fragmented TLS prefixes down to 1–2 bytes, HTTP prefixes arriving in pieces, SSH banners, binary payloads, and an explicit assertion that CloseRawLayer emits both CloseConnection commands.
The P2 is that none of that reaches CI — details inline.
P3, redundant configuration. entrypoint.sh already passes --set rawtcp=false on the mitmdump command line, and the configure() hook sets ctx.options.rawtcp = False again. It is guarded and has no side effect, so this is only a note: two places now express the same setting, and a future change to either one can silently disagree with the other.
The kill_flow fallback on tcp_start / tcp_message and its behaviour were already raised in earlier reviews on this PR; not repeating that here.
This review was AI-assisted. It is not a substitute for independent human review by a committer.
83a4283 to
db3c117
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Incremental review at exact head db3c117b25079e27fccbc539eee5c28f4ecdb1fa, focused on what changed since the last reviewed head.
The previously-raised P2 is genuinely closed at the wiring level. ci.yml:188-198 now pulls and builds the image when Eval is selected and forces the live env flag, and a failure there fails the job — the live suite is no longer excluded from CI by construction. Two new findings, though, and the first one affects ordinary traffic.
[P1] A normally-fragmented TLS ClientHello is killed as raw TCP — packages/eval/harbor/egress_filter.py:133-134, 259-265
def starts_like_tls_record(data: bytes) -> bool:
return len(data) >= 3 and data[0] == 0x16 and data[1] == 0x03looks_like_raw_tcp returns early with False only when this is true. With one byte (0x16) or two (0x16 0x03) received so far, the length test fails, so the TLS escape hatch doesn't fire. Control flow then reaches _still_could_be_http(data_client), which is False for a control byte — no valid request line begins with 0x16. looks_like_raw_tcp therefore returns True, and next_layer:195-200 immediately installs CloseRawLayer.
The connection is killed before the third byte arrives, so the bytes that would have identified it as TLS never get a chance to. This is an ordinary path — TCP is free to deliver a ClientHello in small segments, and no malformed or hostile input is required.
The docstring above the function shows the author already reasoned about exactly this hazard for HTTP: copying mitmproxy's probably_no_http would misclassify GET / GET while the request line is still arriving, so the code deliberately keeps waiting. TLS needs the same treatment and currently doesn't get it.
Suggested shape: make the classifier tri-state rather than boolean — raw / not-raw / undecided — and return undecided while the buffer is still a viable TLS prefix (data == b"\x16", or data == b"\x16\x03"). That matches how the HTTP side already behaves: don't decide until the evidence can exist.
Verified against the pinned 12.2.3 image: the detector returns False for 16, False for 1603, and True only at 160301.
[P2] The live harness doesn't prove the origin socket is closed — packages/eval/harbor/test_egress_filter_live.py:294-305, 411-430
The test waits for the client side to see EOF/reset and asserts the origin received zero payload bytes plus the expected audit record. Neither establishes that the origin-side socket was closed: the origin stats carry no peer-EOF or close counter, so a leaked origin connection that simply never receives data would pass this test unchanged.
The unit test's two CloseConnection commands aren't a substitute — they prove the addon emitted both, not that the real mitmproxy loop closed both ends. Recording and waiting on EOF at the origin would close the gap, along with a case that is explicitly a non-WebSocket upgrade so the tunnel path is exercised deliberately rather than incidentally.
CI status
Exact-head audit is green, but test is completed/cancelled — the run (32623743066) was cancelled during Install dependencies, so the live step was skipped. There is no hosted exact-head evidence that the newly-wired live suite passes. Worth a re-run before this is considered gated.
Validation scope. Local runs at this head: unit 18/18, planner 29/29, Docker live 5/5. Those five live cases don't cover either finding above — neither a fragmented ClientHello nor an origin-side close assertion is among them, which is why both survived a green local run.
db3c117 to
a8c02f2
Compare
|
@Astro-Han Rebased onto current Exact-head local validation:
The 5 live proxy cases remain explicitly skipped locally because Docker is unavailable; the Eval-selected CI lane builds the pinned proxy image and runs them with The fresh workflows are waiting for maintainer approval: CI 32638152317 and Dependency audit 32638152278. Please approve both exact-head runs. |
|
Re-anchoring after the rebase: the head has moved to
One clarification on the P1 while re-reading it: the fallback definition is only used when |
a8c02f2 to
0f4de78
Compare
|
Thanks for re-anchoring both findings. Fixed in
Local validation is green: egress filter unit tests (18/18), CI planner tests (29/29), Eval TypeScript build, embedded origin-script compilation, ASF header check, and diff check. The Docker-backed live suite contains 6 cases but was skipped locally because Docker is unavailable. The new CI and Dependency audit runs are currently waiting for maintainer approval after the rebase/force-update. Once approved, hosted CI will exercise the Docker-backed cases. |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for taking both items head-on. The origin-close proof is genuinely closed now, and I want to be specific about why, because it is the part that was hardest to get right. The remaining item still blocks.
The origin-close finding is closed
packages/eval/harbor/test_egress_filter_live.py:51-126 increments its counters inside the origin's own finally block when the raw-socket and 101-handler receive loops exit, and :445-457,491-516 read those counters from the origin container independently, wait for them to advance, and only then assert *_recv == 0 with no banner and an audit hit. That is an independent observation of the origin peer, not client EOF, handler cancellation, server close, or a zero-length payload standing in as self-proof. Verified by running the raw CONNECT and non-WebSocket 101 cases directly: 2/2 green in 3.096s.
[P1] Fragmented 1- and 2-byte TLS record prefixes still hang on the real mitmproxy path
egress_filter.py:268-273 now uses _could_start_tls_record to withhold CloseRawLayer for a leading 16 or 16 03, so the connection is no longer torn down early. But nothing re-drives classification once the rest of the ClientHello arrives: the real mitmproxy 12.2.3 classifier does not re-decide or complete the handshake, so the connection moves from "closed too early" to "never resolved".
This PR's own live tests say so. test_egress_filter_live.py:350-399,478-483 covers the 1-byte and 2-byte subcases, and both time out in sock.recv with TLS still at SSLWantReadError. Reproduced independently against the pinned Docker image at this exact head — both subcases time out after 23.194s.
The hosted result agrees: CI run 32639766029 attempt 2 at 0f4de78a2110d0bcddb080efc545082c84a6281b is a terminal failure, and the test check is completed/failure. In that live step, 4 of 6 tests pass and the two failures are exactly these fragmented subcases; every preceding lint, format, build, typecheck and affected-test stage is green, and audit is completed/success. So this is not flaky infrastructure — the required check is red on precisely the behaviour under discussion.
One caveat on the unit-level evidence: the addon and fallback detector tests pass, but they call next_layer a second time by hand. That proves the helper's tri-state logic in isolation; it cannot show that the production layer stack ever performs that second call. The two real-event-loop tests are the ones that speak to production, and they fail.
What the fix needs. The undecided state has to be handed to a layer that will genuinely re-classify and make progress as later bytes arrive, and both real paths must go green. Please also confirm the behaviour holds whether or not mitmproxy.net.tls.starts_like_tls_record is importable — the original finding was anchored on the local 3-byte fallback, so a fix proven only with the upstream helper present would not close it.
ran: the pinned-Docker live suite at this exact head (raw CONNECT and non-WebSocket 101 green in 3.096s; the two fragmented subcases timing out after 23.194s), plus independent verification of the hosted check state for this commit.
did-not-run: anything outside packages/eval/harbor; the nine pre-existing commits were not re-reviewed, since git range-diff shows them patch-equivalent to the previously reviewed revision.
0f4de78 to
6fab34c
Compare
|
Fixed the exact CI failure at head Run The handshake driver now flushes The branch is also rebased onto current |
jackwener
left a comment
There was a problem hiding this comment.
Reviewed exact head 6fab34c0257a1f91364018f74813bf61e1c6c762: NO-GO, with one P1 and one P2 inline.
The long-running ownership dispute is mostly resolved correctly. On the shipped raw CONNECT path, the pre-classification CloseRawLayer is the component that owns both transports and emits both CloseConnection commands. On non-WebSocket HTTP 101, rawtcp=false leaves closure to mitmproxy's HTTP layer. I independently exercised both paths on mitmproxy 12.2.3: the client received no origin banner, the origin independently observed peer EOF, and the raw_tunnel audit was present. The earlier TCPFlow.kill()-as-authority and origin-close-proof findings are therefore closed.
The remaining production blocker is specifically the fragmented TLS transition: returning without assigning a layer is not an undecided state once the built-in addon runs later in the same hook dispatch. The current live test also cannot become green after that is fixed because its client does not trust the per-run proxy CA. Details and reproductions are inline.
Validation at this exact head: fallback and real-mitmproxy unit suites 18/18; CI planner 30/30; git diff --check clean; direct real-mitmproxy probes for raw CONNECT, raw 101, and TLS first fragments. Fresh machine state is OPEN / MERGEABLE / BLOCKED. audit is COMPLETED/SUCCESS; required test is currently IN_PROGRESS on run 32644324917, so there is no exact-head gate-complete basis for APPROVE in addition to the findings above. This is COMMENT-only; no merge action.
6fab34c to
c02400d
Compare
|
Rebased onto current Both blocking review findings are fixed:
Reproduction before the fix used the pinned mitmproxy 12.2.3 lifecycle and showed Final validation on CI-matching Node 24.19.0:
The PR diff contains seven code/test/CI files, with no documentation or Java test files. The Docker-backed proxy suite is selected by CI and remains the final container-level check; the fresh exact-head workflows may require maintainer approval. |
Astro-Han
left a comment
There was a problem hiding this comment.
Approving. The ownership argument holds: on main, next_layer returns immediately unless the layer is already a TCPLayer, and the script hook fires before the built-in classifier assigns one — so on the production CONNECT path that branch never runs and the raw tunnel is never closed. Assigning CloseRawLayer ahead of the classifier, letting the built-in HTTP layer close 101 upgrades via rawtcp=false, and keeping the isinstance branch for already-classified paths puts each close where that socket is actually owned, with no second authority.
I checked the parts most likely to hide a leak and found none: _next_layer_bytes returning b"" on error falls through to the built-in classifier rather than skipping the close, CloseRawLayer yielding CloseConnection twice is harmless, kill_flow swallowing exceptions only ever costs an extra attempt, and apply_http_policy fails closed with a 503.
The +995/-10 is not new machinery. Production logic is egress_filter.py +139/-5; the bulk is a real Docker/mitmproxy live suite (606 lines) plus unit tests (+215) and the CI wiring to run them.
One behaviour worth stating explicitly, because I went looking for a hole here and it is worth recording that there isn't one. This head commits to TLS on a 1–2 byte fragment prefix and assigns the layers immediately, giving up the chance to reclassify with more data. I tested whether a raw tunnel could exploit that: against the pinned image, four CONNECT payloads — plain, \x16, \x16\x03, \x16\x03\x01, each followed by garbage — were all closed, with zero bytes relayed to the origin and one raw_tunnel audit record each. Pinning the TLS layer makes the fake client fail the handshake, and the teardown still passes through the audited path.
One non-blocking gap is inline.
中文
批准。所有权论证成立:main 上的 next_layer 除非层已经是 TCPLayer 否则立即 return,而 script hook 在内建 classifier 赋层之前触发——所以生产 CONNECT 路径上那个分支永不执行,raw tunnel 从不被关闭。改成在 classifier 之前自己赋 CloseRawLayer、101 升级交给内建 HTTP 层经 rawtcp=false 关闭、isinstance 分支保留给已赋层路径,三处各自落在真正拥有该 socket 的那一层,没有第二份权威。
最容易藏漏关的地方我都查了,没有:_next_layer_bytes 出错返回 b"" 时落回内建 classifier 而不是跳过关闭;CloseRawLayer 重复 yield CloseConnection 无害;kill_flow 吞异常最多多试一次;apply_http_policy 异常走 503 fail-closed。
+995/-10 不是新机制堆叠:生产逻辑只有 egress_filter.py +139/-5,主体是 606 行真 Docker/mitmproxy live 测试、+215 单测和跑它们的 CI 接线。
有一处行为值得写下来,因为我专门去找过这个洞、结果是没有:这个 head 看到 1–2 字节的分片 TLS 前缀就当场钉死 TLS 层,放弃了拿到更多数据后重新判定的机会。我实测了 raw tunnel 能否利用这一点——用 pin 镜像对四种 CONNECT payload(plain、\x16、\x16\x03、\x16\x03\x01,各接垃圾字节)实跑,四种全部被关闭,origin 侧零字节中继,各落一条 raw_tunnel 审计记录。钉死 TLS 层之后伪客户端无法完成握手,拆连接仍然流经被审计的路径。
一条不阻塞的缺口见行内。
| self.assertIn(b"451", header.split(b"\r\n", 1)[0]) | ||
| self.assertIn(b"tbench_domain", header) | ||
|
|
||
| def test_raw_connect_relays_no_bytes_and_is_audited(self) -> None: |
There was a problem hiding this comment.
[P3] The live suite only proves the "does not wrongly kill" direction; the "does not wrongly let through" side of the fragment handling is unasserted.
The cases here cover HTTP/HTTPS forwarding, fragmented handshakes at 1/2/3 bytes completing, blocklist 451, plain raw CONNECT being closed and audited, 101 upgrade auditing, and websocket pass-through. Real TLS and plain raw are both covered — the disguised middle is not.
That matters specifically because next_layer now commits to TLS on a 1–2 byte prefix. A raw tunnel opening with \x16\x03 is still closed today, but only because the fake client then fails mitmproxy's handshake and the built-in teardown runs. Nothing in this file locks that in. If a future mitmproxy version changes its handshake-failure semantics, the behaviour could regress and this suite would stay green.
A variant of test_raw_connect_relays_no_bytes_and_is_audited that sends b"\x16\x03" + garbage and asserts the same zero-relay and audit outcome would close it.
中文
[P3] live 测试只证了"不误杀",分片处理的"不漏放"那一面没有断言。
现有用例覆盖 HTTP/HTTPS 转发、1/2/3 字节分片握手完成、blocklist 451、plain raw CONNECT 关闭并审计、101 升级审计、websocket 放行——真实 TLS 和 plain raw 两端都有,中间的伪装形态没有。
这一点之所以要紧,是因为 next_layer 现在看到 1–2 字节前缀就钉死 TLS 层。以 \x16\x03 开头的 raw tunnel 今天仍会被关闭,但靠的是伪客户端随后握手失败、内建拆连接兜住,这个文件里没有任何东西把它锁住。将来 mitmproxy 换版本、握手失败语义变化,行为可能退化而这套测试仍然全绿。
照 test_raw_connect_relays_no_bytes_and_is_audited 加一个发送 b"\x16\x03" + 垃圾 的变体、断言同样的零中继与审计结果即可。
|
LGTM, merging. I trial-merged this into the current main ( 简体中文LGTM,合并中。 我在当前 main( |
Summary
Raw CONNECT and HTTP 101 non-WebSocket upgrades must not relay bytes, and the proxy must close both transports — not only mark the flow killed.
next_layerruns before the built-in classifier assignsTCPLayer. When client data looks like raw TCP (not TLS/HTTP), assignCloseRawLayerso the built-in NextLayer leaves it alone and both sides getCloseConnection. Incomplete HTTP prefixes (GET,GET) stay unclassified until the request line can be ruled in or out.rawtcp=false(entrypoint +configure) so HTTP 101 non-WebSocket upgrades never start aTCPLayer; mitmproxy closes the client after 101. WebSocket upgrades stay on the websocket layer.tcp_start/tcp_messageremain last-resort for any residualTCPLayer(e.g.tcp_hosts); they are not the primary closing authority.raw_tunnelaudit.Verification
python3.13 harbor/test_egress_filter.py— 18/18python3.13 harbor/test_eval_framework.py— 3/3MAKA_EVAL_EGRESS_PROXY_TEST=1) againstmaka-eval-egress-proxy:12.2.3Checklist
Does this PR entail a change in behavior?
CloseConnectionat the owning layer (CONNECT viaCloseRawLayer, 101 viarawtcp=false), not byTCPFlow.kill()alone